﻿using System;
using System.Data.SqlClient;
using System.Data;
using System.Collections;

namespace DBConnection
{
    /// <summary>
    /// Maintain a SQL Server connection.
    /// </summary>
    public class DBConnection: IDisposable
    {
        private SqlConnection _connection;
        private int _commandTimeout;
        private DataSet _ds;
        private Stack _trans;
        private Stack _tranNames;
        private IsolationLevel _isoLevel;

        #region Connection Properties
        /// <summary>
        /// Get the data source of this connection
        /// </summary>
        public string Source
        {
            get
            {
                return _connection.DataSource;
            }
        }

        /// <summary>
        /// Get name of the database using by this connection.
        /// </summary>
        public string Database
        {
            get
            {
                return _connection.Database;
            }
        }

        /// <summary>
        /// Get the state of this connection.
        /// </summary>
        public string State
        {
            get
            {
                return _connection.State.ToString();
            }
        }

        /// <summary>
        /// Get all tables.
        /// </summary>
        public string[] Tables
        {
            get
            {
                ArrayList tbs = new ArrayList();
                DataTable tb = GetAllTables();
                foreach (DataRow row in tb.Rows)
                    tbs.Add(row[0].ToString());
                return (string[])tbs.ToArray(typeof(string));
            }
        }

        /// <summary>
        /// Get all columns' names in the current database
        /// </summary>
        public string[] Attributes
        {
            get
            {
                ArrayList atts = new ArrayList();
                DataTable tb = GetColumnInfo();
                foreach (DataRow row in tb.Rows)
                    atts.Add(row[0].ToString());
                return (string[])atts.ToArray(typeof(string));
            }
        }

        /// <summary>
        /// Get the current isolation level of ALL transactions using this connection.
        /// </summary>
        public IsolationLevel Isolation
        {
            get
            {
                return _isoLevel;
            }
        }

        /// <summary>
        /// Get the current transactions in this connection in the last-in-first-out order.
        /// </summary>
        public string[] Transactions
        {
            get
            {
                return (string[])_tranNames.ToArray();
            }
        }
        #endregion;

        #region Constructors
        /// <summary>
        /// Create a connection to a specific database on a specific source using predefined information (config.xml).
        /// </summary>
        public DBConnection()
        {
            String user = "", password = "", dbName = "", source = "", type = "", connTimeout = "", commandTimeout = "";
            try
            {
                FileUtility.ReadConfig("config.xml", out source, out user, out password, out dbName, out type, out connTimeout, out commandTimeout);
            }
            catch (Exception exc)
            {
                MyDebugClass.LogThis("Create new instance of DBConnection()", exc);
            }

            string connString = "Data Source=" + source + ";User ID=" + user + ";Password=" + password + ";Initial Catalog=" + dbName + ";Integrated Security=" + type;

            try
            {
                _connection = new SqlConnection(connString);
                _commandTimeout = Int32.Parse(commandTimeout);
                _connection.Open();
                _connection.StateChange += new StateChangeEventHandler(connection_StateChange);
                _ds = new DataSet();
                _trans = new Stack();
                _tranNames = new Stack();
                _isoLevel = IsolationLevel.ReadCommitted;
            }
            catch (Exception exc)
            {
                MyDebugClass.LogThis("Create new instance of DBConnection()", exc);
            }
            finally
            {
                _connection.Close();
            }
        }

        /// <summary>
        /// Create a connection to a specific database on a specific source.
        /// </summary>
        /// <param name="source">Source or server name of target server;</param>
        /// <param name="user">The account used to log in;</param>
        /// <param name="password">Log in password;</param>
        /// <param name="db">Name of target database;</param>
        /// <param name="securityType">Define the authentication to use;</param>
        /// <param name="connTimeout">Define the connection timeout;</param>
        /// <param name="commandTimeout">Define the command timeout.</param>
        public DBConnection(string source, string user, string password, string db, string securityType, string connTimeout, int commandTimeout)
        {
            if (source == String.Empty)
                throw new ArgumentNullException("source");

            string connString = "Data Source=" + source + ";User ID=" + user + ";Password=" + password + ";Initial Catalog=" + db + ";Integrated Security=" + securityType + ";Connection Timeout=" + connTimeout;

            try
            {
                _connection = new SqlConnection(connString);
                _commandTimeout = (commandTimeout > 10) ? commandTimeout : 10;
                _connection.Open();
                _connection.StateChange += new StateChangeEventHandler(connection_StateChange);
                _ds = new DataSet();
                _trans = new Stack();
                _tranNames = new Stack();
                _isoLevel = IsolationLevel.ReadCommitted;
            }
            catch (Exception exc)
            {
                MyDebugClass.LogThis("Create new instance of DBConnection(source,user,password,db)", exc);
                throw new Exception("Error connect to your database! Please check your configuration info!");
            }
            finally
            {
                _connection.Close();
            }
        }

        /// <summary>
        /// Create a connection using given connection string.
        /// </summary>
        /// <param name="connectionString">The connection string used to create connection.</param>
        /// <param name="commandTimeout">Define the command timeout.</param>
        public DBConnection(string connectionString, int commandTimeout)
        {
            try
            {
                _connection = new SqlConnection(connectionString);
                _commandTimeout = (commandTimeout > 10) ? commandTimeout : 10;
                _connection.Open();
                _connection.StateChange += new StateChangeEventHandler(connection_StateChange);
                _ds = new DataSet();
                _trans = new Stack();
                _tranNames = new Stack();
                _isoLevel = IsolationLevel.ReadCommitted;
            }
            catch (Exception exc)
            {
                MyDebugClass.LogThis("Create new instance of DBConnection(connectionString)", exc);
            }
            finally
            {
                _connection.Close();
            }
        }
        #endregion;

        #region Basic functions
        /// <summary>
        /// Open this connection to execute commands.
        /// </summary>
        /// <returns>true if the connection is opened.</returns>
        public bool Open()
        {
            if (_connection.State == ConnectionState.Closed)
                _connection.Open();
            return true;
        }

        /// <summary>
        /// Close this connection.
        /// </summary>
        /// <returns>true if the connection is closed; false if the connection is executing commands.</returns>
        public bool Close()
        {
            if (_connection.State == ConnectionState.Executing
                || _connection.State == ConnectionState.Fetching)
                return false;

            _connection.Close();
            return true;
        }

        protected void connection_StateChange(object sender, StateChangeEventArgs e)
        {
            if (_connection == null)
                return;

            if ((e.OriginalState == ConnectionState.Executing || e.OriginalState == ConnectionState.Fetching) && e.CurrentState == ConnectionState.Broken)
            {
                try
                {
                    _connection.Close();
                    _connection.Open();
                }
                catch (Exception exc)
                {
                    _connection.Close();
                    MyDebugClass.LogThis("Attempting reconnect to " + _connection.DataSource, exc);
                }
            }
        }

        /// <summary>
        /// Change the current database of this connection.
        /// </summary>
        /// <param name="newDb">Name of new database.</param>
        /// <returns>true if the connection's database is changed; false if the new database doesn't exist or the connection is executing commands.</returns>
        public bool ChangeDatabase(string newDb)
        {
            if (newDb.Equals(_connection.Database, StringComparison.OrdinalIgnoreCase))
                return true;

            if (_connection.State == ConnectionState.Executing
                || _connection.State == ConnectionState.Fetching)
                return false;

            try
            {
                _connection.Close();
                _connection.ChangeDatabase(newDb);
            }
            catch (Exception exc)
            {
                MyDebugClass.LogThis("Try to change database to " + newDb, exc);
                return false;
            }
            return true;
        }

        /// <summary>
        /// Begin a transaction.
        /// </summary>
        /// <param name="isolationLevel">The isolation level of this transaction, which could be one of the followings: readcommitted, readuncommitted, repeatableread, serializable, snapshot, chaos</param>
        /// <param name="transName">Name of the transaction.</param>
        public bool BeginTransaction(string isolationLevel, string transName)
        {
            transName = transName.ToLower();
            if (_tranNames.Contains(transName))
                return false;

            try
            {
                if (isolationLevel != null && isolationLevel != "")
                {
                    switch (isolationLevel.ToLower())
                    {
                        case "readcommitted":
                            _isoLevel = IsolationLevel.ReadCommitted;
                            break;
                        case "readuncommitted":
                            _isoLevel = IsolationLevel.ReadUncommitted;
                            break;
                        case "repeatableread":
                            _isoLevel = IsolationLevel.RepeatableRead;
                            break;
                        case "serializable":
                            _isoLevel = IsolationLevel.Serializable;
                            break;
                        case "snapshot":
                            _isoLevel = IsolationLevel.Snapshot;
                            break;
                        case "chaos":
                            _isoLevel = IsolationLevel.Chaos;
                            break;
                        case "unspecified":
                            _isoLevel = IsolationLevel.Unspecified;
                            break;
                        default:
                            _isoLevel = IsolationLevel.ReadCommitted;
                            break;
                    }
                }

                _trans.Push(_connection.BeginTransaction(_isoLevel, transName));
                _tranNames.Push(transName);
                return true;
            }
            catch (Exception exc)
            {
                MyDebugClass.LogThis("Try to begin a new transaction named " + transName
                    + " with the isolation level is " + isolationLevel, exc);
                throw exc;
            }
        }

        /// <summary>
        /// Create a savepoint inside current transaction.
        /// </summary>
        /// <param name="savepoint">Name of the savepoint.</param>
        public bool SetSavepoint(string savepoint)
        {
            savepoint = savepoint.ToLower();
            if (_tranNames.Contains("Savepoint " + savepoint))
                return false;

            ((SqlTransaction)_trans.Peek()).Save(savepoint);
            //ExecNonQuery("SAVE TRANSACTION " + savepoint);
            _tranNames.Push("Savepoint " + savepoint);
            return true;
        }

        /// <summary>
        /// Commit a transaction.
        /// </summary>
        /// <param name="transName">Transaction name.</param>
        public bool Commit(string transName)
        {
            try
            {
                transName = transName.ToLower();
                if (!_tranNames.Contains(transName))
                    return false;

                //ExecNonQuery("COMMIT TRANSACTION " + transName);
                while (_tranNames.Peek().ToString() != transName)
                {
                    _trans.Pop();
                    _tranNames.Pop();
                }
                ((SqlTransaction)_trans.Peek()).Commit();
                _trans.Pop();
                _tranNames.Pop();
                return true;
            }
            catch (Exception exc)
            {
                MyDebugClass.LogThis("Try to commit " + transName, exc);
                return false;
            }
        }

        /// <summary>
        /// Rollback to the beginning of the transaction or a specific savepoint.
        /// </summary>
        /// <param name="transName">Name of the transaction or the savepoint to rollback;</param>
        /// <param name="isTransaction">True indicates that you want to rollback to a transaction; False indicates that you want to rollback to a specific savepoint.</param>
        public bool Rollback(string transName, bool isTransaction)
        {
            try
            {
                transName = transName.ToLower();
                // Commit to a transaction.
                if (isTransaction)
                {
                    if (!_tranNames.Contains(transName))
                        return false;

                    //ExecNonQuery("ROLLBACK " + transName);
                    while (_tranNames.Peek().ToString() != transName)
                    {
                        _trans.Pop();
                        _tranNames.Pop();
                    }
                    ((SqlTransaction)_trans.Peek()).Rollback();
                    _trans.Pop();
                    _tranNames.Pop();
                    return true;
                }

                // Rollback to a savepoint.
                transName = "Savepoint " + transName;
                if (!_tranNames.Contains(transName))
                    return false;

                //ExecNonQuery("ROLLBACK " + transName);
                int passTrans = 0;
                while (_tranNames.Peek().ToString() != transName)
                {
                    if (!_tranNames.Peek().ToString().StartsWith("Savepoint "))
                        ++passTrans;
                    _tranNames.Pop();
                }

                for (int i = 0; i < passTrans; i++)
                    _trans.Pop();

                ((SqlTransaction)_trans.Peek()).Rollback(transName.Remove(0, 10));
                _trans.Pop();

                while (_tranNames.Peek().ToString().StartsWith("Savepoint "))
                    _tranNames.Pop();
                _tranNames.Pop();
                return true;
            }
            catch (Exception exc)
            {
                MyDebugClass.LogThis("Try to rollback to " + transName, exc);
                return false;
            }
        }
        #endregion;

        #region Functions
        /// <summary>
        /// Execute a stored procedure and return the number of affected rows.
        /// </summary>
        /// <param name="spName">Name of stored procedure to execute, </param>
        /// <param name="parametersArr">An array of input parameters (name), </param>
        /// <param name="parametersValue">An array of input parameters (value).</param>
        /// <returns>The number of affected rows by executing target procedure.</returns>
        public int Sp_ExecNonQuery(string spName, string[] parametersArr, string[] parametersValue)
        {
            SqlCommand sqlCommand = new SqlCommand();
            sqlCommand.CommandType = System.Data.CommandType.StoredProcedure;
            sqlCommand.Connection = _connection;
            sqlCommand.CommandText = spName;
            sqlCommand.CommandTimeout = _commandTimeout;
            if (_trans.Count > 0)
                sqlCommand.Transaction = (SqlTransaction)_trans.Peek();
            if (parametersArr != null && parametersValue != null)
            {
                for (int i = 0; i < parametersArr.Length; i++)
                {
                    sqlCommand.Parameters.Add(SafeSqlLiteral(parametersArr[i]), System.Data.SqlDbType.NVarChar);
                    sqlCommand.Parameters[SafeSqlLiteral(parametersArr[i])].Value = SafeSqlLiteral(parametersValue[i]);
                }
            }

            try
            {
                if (_connection.State == ConnectionState.Closed
                    || _connection.State == ConnectionState.Broken)
                    return -1;
                return sqlCommand.ExecuteNonQuery();
            }
            catch (Exception exc)
            {
                string action = "Executing stored procedure " + spName + " in " + _connection.DataSource + "." + _connection.Database;
                MyDebugClass.LogThis(action, exc);
                throw exc;
            }
        }

        /// <summary>
        /// Execute a stored procedure and return the result of that procedure.
        /// </summary>
        /// <param name="spName">Name of stored procedure to execute, </param>
        /// <param name="parametersArr">An array of input parameters (name), </param>
        /// <param name="parametersValue">An array of input parameters (value).</param>
        /// <returns>An object produced by the stored procedure</returns>
        public object Sp_ExecScalar(string spName, string[] parametersArr, string[] parametersValue)
        {
            SqlCommand sqlCommand = new SqlCommand();
            sqlCommand.CommandType = System.Data.CommandType.StoredProcedure;
            sqlCommand.Connection = _connection;
            sqlCommand.CommandText = spName;
            sqlCommand.CommandTimeout = _commandTimeout;
            if (_trans.Count > 0)
                sqlCommand.Transaction = (SqlTransaction)_trans.Peek();
            if (parametersArr != null && parametersValue != null)
            {
                for (int i = 0; i < parametersArr.Length; i++)
                {
                    sqlCommand.Parameters.Add(SafeSqlLiteral(parametersArr[i]), System.Data.SqlDbType.NVarChar);
                    sqlCommand.Parameters[SafeSqlLiteral(parametersArr[i])].Value = SafeSqlLiteral(parametersValue[i]);
                }
            }

            try
            {
                if (_connection.State == ConnectionState.Closed
                    || _connection.State == ConnectionState.Broken)
                    return null;
                return sqlCommand.ExecuteScalar();
            }
            catch (Exception exc)
            {
                string action = "Executing stored procedure " + spName + " in " + _connection.DataSource + "." + _connection.Database;
                MyDebugClass.LogThis(action, exc);
                throw exc;
            }
        }

        /// <summary>
        /// Execute a stored procedure and return a table produced by that procedure.
        /// </summary>
        /// <param name="spName">Name of stored procedure to execute, </param>
        /// <param name="tbName">Name of returned table, </param>
        /// <param name="parametersArr">An array of input parameters (name), </param>
        /// <param name="parametersValue">An array of input parameters (value).</param>
        /// <returns>A table produced by the procedure.</returns>
        public DataTable Sp_ExecReader(string spName, string tbName, string[] parametersArr, string[] parametersValue)
        {
            if (_ds.Tables.IndexOf(tbName) != -1)
            {
                _ds.Tables.Remove(tbName);
            }

            SqlCommand sqlCommand = new SqlCommand();
            sqlCommand.CommandType = System.Data.CommandType.StoredProcedure;
            sqlCommand.Connection = _connection;
            sqlCommand.CommandText = spName;
            sqlCommand.CommandTimeout = _commandTimeout;
            if (_trans.Count > 0)
                sqlCommand.Transaction = (SqlTransaction)_trans.Peek();

            if (parametersArr != null && parametersValue != null)
            {
                for (int i = 0; i < parametersArr.Length; i++)
                {
                    sqlCommand.Parameters.Add(SafeSqlLiteral(SafeSqlLiteral(parametersArr[i])), System.Data.SqlDbType.NVarChar);
                    sqlCommand.Parameters[SafeSqlLiteral(SafeSqlLiteral(parametersArr[i]))].Value = SafeSqlLiteral(parametersValue[i]);
                }
            }

            SqlDataAdapter da = new SqlDataAdapter(sqlCommand);
            try
            {
                if (_connection.State == ConnectionState.Closed
                    || _connection.State == ConnectionState.Broken)
                    return null;
                da.Fill(_ds, tbName);
                return _ds.Tables[tbName];
            }
            catch (Exception exc)
            {
                string action = "Executing stored procedure " + spName + " in " + _connection.DataSource + "." + _connection.Database;
                MyDebugClass.LogThis(action, exc);
                throw exc;
            }
        }

        /// <summary>
        /// Retrieves the table that produced by a command from a database's original table.
        /// </summary>
        /// <param name="sqlCommand">A sql select command, </param>
        /// <param name="tableName">Name of the target table.</param>
        /// <returns>Result table.</returns>
        public DataTable GetDataTable(string sqlSelect, string tbName)
        {
            if (_ds.Tables.IndexOf(tbName) != -1)
                _ds.Tables.Remove(tbName);

            sqlSelect = SafeSqlLiteral(sqlSelect);
            try
            {
                if (_connection.State == ConnectionState.Closed
                    || _connection.State == ConnectionState.Broken)
                    return null;
                SqlDataAdapter adapter = new SqlDataAdapter(sqlSelect, _connection);
                adapter.Fill(_ds, tbName);
            }
            catch (Exception exc)
            {
                string action = "Executing the query: " + sqlSelect + " in " + _connection.DataSource + "." + _connection.Database;
                MyDebugClass.LogThis(action, exc);
                throw exc;
            }

            return _ds.Tables[tbName];
        }

        /// <summary>
        /// Execute a SQL command and return the number of affected rows.
        /// </summary>
        /// <param name="sqlCommand">a SQL command.</param>
        /// <returns>The number of affected rows.</returns>
        public int ExecNonQuery(string sqlCommand)
        {
            try
            {
                if (_connection.State == ConnectionState.Closed
                    || _connection.State == ConnectionState.Broken)
                    return -1;

                sqlCommand = SafeSqlLiteral(sqlCommand);
                SqlCommand command = new SqlCommand(sqlCommand, _connection);
                command.CommandTimeout = _commandTimeout;
                if (_trans.Count > 0)
                    command.Transaction = (SqlTransaction)_trans.Peek();

                return command.ExecuteNonQuery();
            }
            catch (Exception exc)
            {
                string action = "Executing the query: " + sqlCommand + " in " + _connection.DataSource + "." + _connection.Database;
                MyDebugClass.LogThis(action, exc);
                throw exc;
            }
        }

        /// <summary>
        /// Execute a SQL command and return the value of the first column of first row
        /// in the result table.
        /// </summary>
        /// <param name="sqlCommand">a SQL command.</param>
        /// <returns>An object.</returns>
        public object ExecScalar(string sqlCommand)
        {
            try
            {
                if (_connection.State == ConnectionState.Closed
                    || _connection.State == ConnectionState.Broken)
                    return null;

                sqlCommand = SafeSqlLiteral(sqlCommand);
                SqlCommand command = new SqlCommand(sqlCommand, _connection);
                command.CommandTimeout = _commandTimeout;
                if (_trans.Count > 0)
                    command.Transaction = (SqlTransaction)_trans.Peek();

                return command.ExecuteScalar();
            }
            catch (Exception exc)
            {
                string action = "Executing the query: " + sqlCommand + " in " + _connection.DataSource + "." + _connection.Database;
                MyDebugClass.LogThis(action, exc);
                throw exc;
            }
        }

        /// <summary>
        /// Return a SqlDataReader using the current connection, the given sqlCommand and the command behavior.
        /// </summary>
        /// <param name="sqlCommand">The sql command to execute;</param>
        /// <param name="behavior">Behavior of the reader.</param>
        /// <returns>SqlDataReader</returns>
        public SqlDataReader ExecReader(string sqlCommand, CommandBehavior behavior)
        {
            try
            {
                if (_connection.State == ConnectionState.Closed
                    || _connection.State == ConnectionState.Broken)
                    return null;

                sqlCommand = SafeSqlLiteral(sqlCommand);
                SqlCommand command = new SqlCommand(sqlCommand, _connection);
                command.CommandTimeout = _commandTimeout;
                if (_trans.Count > 0)
                    command.Transaction = (SqlTransaction)_trans.Peek();

                return command.ExecuteReader(behavior);
            }
            catch (Exception exc)
            {
                string action = "Executing the reader: " + sqlCommand + " in " + _connection.DataSource + "." + _connection.Database;
                MyDebugClass.LogThis(action, exc);
                throw exc;
            }
        }
        #endregion;

        #region Security (Incomplete)
        /// <summary>
        /// Replace potential security risks (such as ; -- /* */ characters) in given string.
        /// </summary>
        /// <param name="inputSQL">An input string to validate.</param>
        /// <returns>A safe string.</returns>
        public static string SafeSqlLiteral(string inputSQL)
        {
            inputSQL = inputSQL.Replace(";", "");
            inputSQL = inputSQL.Replace("--", "");
            inputSQL = inputSQL.Replace("/*", "");
            inputSQL = inputSQL.Replace("*/", "");
            return inputSQL;
        }

        private static string SafeSqlCommand(string sqlCommand)
        {
            if (sqlCommand.StartsWith("delete ") || sqlCommand.StartsWith("drop table "))
                return "";

            // select, insert, update, exec, execute, use, set datetime
            // TODO: Implement SafeSqlCommand to prevent SQL injection.

            return sqlCommand;
        }
        #endregion;

        #region Database's tables and columns
        /// <summary>
        /// Get all columns' information, include: columns (table_name.column_name), is_identity, column_default, is_nullable, data_type.
        /// </summary>
        /// <returns>A DataTable contains all columns' info.</returns>
        public DataTable GetColumnInfo()
        {
            if (_ds.Tables.IndexOf("columns") != -1)
                _ds.Tables.Remove("columns");

            try
            {
                if (_connection.State == ConnectionState.Closed
                    || _connection.State == ConnectionState.Broken)
                    return null;

                SqlDataAdapter adapter = new SqlDataAdapter("select TABLE_NAME + '.' + COLUMN_NAME as columns, ColumnProperty(OBJECT_ID(TABLE_SCHEMA + '.' + TABLE_NAME), COLUMN_NAME, 'IsIdentity') as is_indentity, column_default, is_nullable, data_type from INFORMATION_SCHEMA.COLUMNS where TABLE_NAME <> 'sysdiagrams'", _connection);
                adapter.Fill(_ds, "columns");
                return _ds.Tables["columns"];
            }
            catch (Exception exc)
            {
                string action = "Attempting get all columns' info";
                MyDebugClass.LogThis(action, exc);
                return null;
            }
        }

        /// <summary>
        /// Get all tables in the current database
        /// </summary>
        /// <returns></returns>
        public DataTable GetAllTables()
        {
            if (_ds.Tables.IndexOf("tables") != -1)
                _ds.Tables.Remove("tables");

            try
            {
                if (_connection.State == ConnectionState.Closed
                    || _connection.State == ConnectionState.Broken)
                    return null;

                SqlDataAdapter adapter = new SqlDataAdapter("select TABLE_NAME from INFORMATION_SCHEMA.COLUMNS where TABLE_NAME <> 'sysdiagrams' order by table_name", _connection);
                adapter.Fill(_ds, "tables");
                return _ds.Tables["tables"];
            }
            catch (Exception exc)
            {
                string action = "Attempting get all tables in the database " + _connection.Database;
                MyDebugClass.LogThis(action, exc);
                return null;
            }
        }
        #endregion;

        #region IDisposable Members

        public void Dispose()
        {
            if (_ds != null)
            {
                _ds.Clear();
                _ds.Dispose();
            }
            if (_connection != null)
            {
                _connection.Close();
                _connection.Dispose();
            }
        }

        void IDisposable.Dispose()
        {
            if (_ds != null)
            {
                _ds.Clear();
                _ds.Dispose();
            }
            if (_connection != null)
            {
                _connection.Close();
                _connection.Dispose();
            }
        }

        #endregion
    }
}
